Исходные данные: датасет с действиями пользователей, техническое задание и несколько вспомогательных датасетов
Цель: провести оценку результатов A/B-теста
Основные задачи:
1) Оценить корректность проведения теста:
проверить пересечение тестовой аудитории с конкурирующим тестом,
проверить совпадение теста и маркетинговых событий, другие проблемы временных границ теста.
2) Проанализируровать результаты теста.
Техническое задание (ТЗ):
recommender_system_test;product_page,product_cart,purchase.# импорт необходимых библиотек
import pandas as pd
import plotly.express as px
import matplotlib.pyplot as plt
import seaborn as sns
import datetime
from plotly import graph_objects as go
from scipy import stats as st
import math as mth
import warnings
warnings.filterwarnings('ignore')
# чтение данных из файла
events, marketing_events, new_users, participants = (
pd.read_csv('/Users/olgakozlova/Desktop/datasets/final_ab_events.csv'),
pd.read_csv('/Users/olgakozlova/Desktop/datasets/ab_project_marketing_events.csv'),
pd.read_csv('/Users/olgakozlova/Desktop/datasets/final_ab_new_users.csv'),
pd.read_csv('/Users/olgakozlova/Desktop/datasets/final_ab_participants.csv')
)
display(events.head())
display(marketing_events.head())
display(new_users.head())
participants.head()
| user_id | event_dt | event_name | details | |
|---|---|---|---|---|
| 0 | E1BDDCE0DAFA2679 | 2020-12-07 20:22:03 | purchase | 99.99 |
| 1 | 7B6452F081F49504 | 2020-12-07 09:22:53 | purchase | 9.99 |
| 2 | 9CD9F34546DF254C | 2020-12-07 12:59:29 | purchase | 4.99 |
| 3 | 96F27A054B191457 | 2020-12-07 04:02:40 | purchase | 4.99 |
| 4 | 1FD7660FDF94CA1F | 2020-12-07 10:15:09 | purchase | 4.99 |
| name | regions | start_dt | finish_dt | |
|---|---|---|---|---|
| 0 | Christmas&New Year Promo | EU, N.America | 2020-12-25 | 2021-01-03 |
| 1 | St. Valentine's Day Giveaway | EU, CIS, APAC, N.America | 2020-02-14 | 2020-02-16 |
| 2 | St. Patric's Day Promo | EU, N.America | 2020-03-17 | 2020-03-19 |
| 3 | Easter Promo | EU, CIS, APAC, N.America | 2020-04-12 | 2020-04-19 |
| 4 | 4th of July Promo | N.America | 2020-07-04 | 2020-07-11 |
| user_id | first_date | region | device | |
|---|---|---|---|---|
| 0 | D72A72121175D8BE | 2020-12-07 | EU | PC |
| 1 | F1C668619DFE6E65 | 2020-12-07 | N.America | Android |
| 2 | 2E1BF1D4C37EA01F | 2020-12-07 | EU | PC |
| 3 | 50734A22C0C63768 | 2020-12-07 | EU | iPhone |
| 4 | E1BDDCE0DAFA2679 | 2020-12-07 | N.America | iPhone |
| user_id | group | ab_test | |
|---|---|---|---|
| 0 | D1ABA3E2887B6A73 | A | recommender_system_test |
| 1 | A7A3664BD6242119 | A | recommender_system_test |
| 2 | DABC14FDDFADD29E | A | recommender_system_test |
| 3 | 04988C5DF189632E | A | recommender_system_test |
| 4 | 482F14783456D21B | B | recommender_system_test |
# функция для вывода: инфо, количество пропущенных значений, количество полных дубликатов
def df_overview(df):
print('Общая информация о данных:\n')
df.info()
print('\nКоличество пропусков:\n')
display(df.isna().sum())
print('\nКоличество полных дубликатов:', df.duplicated().sum())
Действия новых пользователей с 07.12.2020 по 04.01.2021 год
Согласно описанию к данным:
user_id — идентификатор пользователя;event_dt — дата и время покупки;event_name — тип события;details — дополнительные данные о событии. Например, для покупок, purchase, в этом поле хранится стоимость покупки в долларах.# вызов функции df_overview
df_overview(events)
Общая информация о данных: <class 'pandas.core.frame.DataFrame'> RangeIndex: 440317 entries, 0 to 440316 Data columns (total 4 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 user_id 440317 non-null object 1 event_dt 440317 non-null object 2 event_name 440317 non-null object 3 details 62740 non-null float64 dtypes: float64(1), object(3) memory usage: 13.4+ MB Количество пропусков:
user_id 0 event_dt 0 event_name 0 details 377577 dtype: int64
Количество полных дубликатов: 0
Тип данных столбца 'event_dt' (дата и время покупки) преобразуем из object в datetime:
# преобразование данных о времени
events['event_dt'] = pd.to_datetime(events['event_dt'], format='%Y-%m-%d %H:%M:%S')
# проверка типов данных после преобразования строки в дату и время
events.dtypes
user_id object event_dt datetime64[ns] event_name object details float64 dtype: object
Таблица events содержит данные о действия новых пользователей в период с 07 декабря 2020 года по 4 января 2021 года.
Таблица состоит из 4-х столбцов и 440317 строк. Для столбца 'event_dt' тип данных преобразован в datetime, для остальных значений столбца типы данных соответствуют заявленным.
Полные дубликаты в данных не обнаружены.
В столбце 'details' обнаружено 377 577 пропущенных значений. Однако, согласно документации, в этом столбце хранятся дополнительные данные о событии (например, стоимость покупки), поэтому принято решение оставить пропуски без изменения.
Календарь маркетинговых событий на 2020 год
Согласно описанию к данным:
name — название маркетингового события;
regions — регионы, в которых будет проводиться рекламная кампания;
start_dt — дата начала кампании;
finish_dt — дата завершения кампании.
# вызов функции df_overview
df_overview(marketing_events)
Общая информация о данных: <class 'pandas.core.frame.DataFrame'> RangeIndex: 14 entries, 0 to 13 Data columns (total 4 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 name 14 non-null object 1 regions 14 non-null object 2 start_dt 14 non-null object 3 finish_dt 14 non-null object dtypes: object(4) memory usage: 576.0+ bytes Количество пропусков:
name 0 regions 0 start_dt 0 finish_dt 0 dtype: int64
Количество полных дубликатов: 0
Тип данных столбцов 'start_dt' 'finish_dt' (дата начала и завершения кампании) необходимо преобразовать из object в datetime:
# преобразование данных о времени
marketing_events['start_dt'] = pd.to_datetime(marketing_events['start_dt'], format='%Y-%m-%d')
marketing_events['finish_dt'] = pd.to_datetime(marketing_events['finish_dt'], format='%Y-%m-%d')
# проверка типов данных после преобразования строки в дату
marketing_events.dtypes
name object regions object start_dt datetime64[ns] finish_dt datetime64[ns] dtype: object
Таблица marketing_events содержит данные о маркетинговых событиях 2020 года.
Таблица состоит из 4-х столбцов и 14 строк. Для столбцов 'start_dt' и 'finish_dt' тип данных изменен на datetime.
Пропущенные значения и полные дубликаты в данных не обнаружены.
Пользователи, зарегистрировавшиеся с 07 по 21 декабря 2020 года
Согласно документации к данным:
user_id — идентификатор пользователя;
first_date — дата регистрации;
region — регион пользователя;
device — устройство, с которого происходила регистрация.
# вызов функции df_overview
df_overview(new_users)
Общая информация о данных: <class 'pandas.core.frame.DataFrame'> RangeIndex: 61733 entries, 0 to 61732 Data columns (total 4 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 user_id 61733 non-null object 1 first_date 61733 non-null object 2 region 61733 non-null object 3 device 61733 non-null object dtypes: object(4) memory usage: 1.9+ MB Количество пропусков:
user_id 0 first_date 0 region 0 device 0 dtype: int64
Количество полных дубликатов: 0
Тип данных столбца 'first_date' (дата регистрации) преобразуем из object в datetime:
# преобразование данных о времени
new_users['first_date'] = pd.to_datetime(new_users['first_date'], format='%Y-%m-%d')
# добавление столбца даты регистрации для получения даты без времени 00:00:00
#new_users['registration_date'] = new_users['first_date'].dt.date
# проверка типов данных после преобразования строки в дату
new_users.dtypes
user_id object first_date datetime64[ns] region object device object dtype: object
Таблица new_users содержит данные зарегистрировавшихся пользователях с 7 по 21 декабря 2020 года (устройство, регион, id, дата регистрации).
Таблица состоит из 4-х столбцов и 61 733 строк. У столбца 'first_date' тип данных изменен на datetime.
Пропущенные значения и полные дубликаты в данных не обнаружены.
Таблица участников теста
Согласно описанию к данным:
user_id — идентификатор пользователя;
ab_test — название теста;
group — группа пользователя.
# вызов функции df_overview
df_overview(participants)
Общая информация о данных: <class 'pandas.core.frame.DataFrame'> RangeIndex: 18268 entries, 0 to 18267 Data columns (total 3 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 user_id 18268 non-null object 1 group 18268 non-null object 2 ab_test 18268 non-null object dtypes: object(3) memory usage: 428.3+ KB Количество пропусков:
user_id 0 group 0 ab_test 0 dtype: int64
Количество полных дубликатов: 0
Загруженные данные содержат информацию о действиях пользователей, о маркетинговых событиях 2020 года, о проведенных тестах;
В столбце 'details' таблицы 'events' обнаружены пропуски в количестве: 377 577. Принято решение оставить пропущенные значения без изменения, так как согласно документации, в этом столбце хранятся дополнительные данные о событии (например, стоимость покупки);
В колонках с датой и временем тип данных object преобразован в datetime;
Полные дубликаты в данных не обнаружены.
Для оценки корректности проведения теста необходимо удостовериться, что данные соответствуют требованиям ТЗ. А также в целом проверить корректность всех пунктов технического задания. Рассмотрим подробнее каждый датасет.
Пользователи, зарегистрировавшиеся с 7 по 21 декабря 2020 года
new_users
| user_id | first_date | region | device | |
|---|---|---|---|---|
| 0 | D72A72121175D8BE | 2020-12-07 | EU | PC |
| 1 | F1C668619DFE6E65 | 2020-12-07 | N.America | Android |
| 2 | 2E1BF1D4C37EA01F | 2020-12-07 | EU | PC |
| 3 | 50734A22C0C63768 | 2020-12-07 | EU | iPhone |
| 4 | E1BDDCE0DAFA2679 | 2020-12-07 | N.America | iPhone |
| ... | ... | ... | ... | ... |
| 61728 | 1DB53B933257165D | 2020-12-20 | EU | Android |
| 61729 | 538643EB4527ED03 | 2020-12-20 | EU | Mac |
| 61730 | 7ADEE837D5D8CBBD | 2020-12-20 | EU | PC |
| 61731 | 1C7D23927835213F | 2020-12-20 | EU | iPhone |
| 61732 | 8F04273BB2860229 | 2020-12-20 | EU | Android |
61733 rows × 4 columns
new_users['user_id'].nunique()
61733
Все пользователи уникальны. Визуалзируем распределение пользователей по регионам:
# регионы и число пользователей
new_users['region'].value_counts()
EU 46270 N.America 9155 CIS 3155 APAC 3153 Name: region, dtype: int64
sns.set_theme(style="whitegrid")
plt.figure(figsize=(15,5))
sns.histplot(data=new_users, x='region',)
plt.title('Распределение пользователей по регионам', fontsize=15)
plt.xlabel('Регионы')
plt.ylabel('Число пользователей')
plt.show()
Среди зарегистрировавшихся пользователей присутствуют жители Европейского союза, Северной Америки, Азиатско-Тихоокеанского региона и СНГ. Согласно ТЗ, нас интересуют только пользователи Европейского союза:
# фильтрация пользователей по региону EU
new_users = new_users.query('region == "EU"')
Посмотрим, как пользователи распределены по дате регистрации:
# столбчатая диаграмма Распределение регистраций пользователей во времени
sns.set_theme(style="whitegrid")
new_users.groupby('first_date').agg({'user_id': 'count'}).plot(kind = 'bar', figsize = (15, 5))
plt.title('Распределение регистраций пользователей во времени', fontsize=15)
plt.xlabel('Дата')
plt.ylabel('Количество пользователей')
plt.xticks(rotation = 30)
plt.show()
Дата остановки набора новых пользователей не соответствует ТЗ, вместо 2020-12-21 на графике можно наблюдать дату 2020-12-23. С датой начала набора все в порядке.
Отберем только тех пользователей, которые подходят под условия задания:
# фильтрация пользоватей по дате окончания набора
new_users = new_users.query('first_date <= "2020-12-21"')
new_users.shape
(42340, 4)
# столбчатая диаграмма Распределение регистраций пользователей во времени
sns.set_theme(style="whitegrid")
new_users.groupby('first_date').agg({'user_id': 'count'}).plot(kind = 'bar', figsize = (15, 5))
plt.title('Распределение регистраций пользователей во времени', fontsize=15)
plt.xlabel('Дата')
plt.ylabel('Количество пользователей')
plt.xticks(rotation = 30)
plt.show()
Осталось 42 340 пользователей из региона EU, которые зарегистрировались в период с 2020-12-07 по 2020-12-21. Посмотрим, какие устройства пользователи использовали при регистрации:
# устройства, с которых регистрировались пользователи
sns.set_theme(style="whitegrid")
plt.figure(figsize=(15,5))
sns.histplot(data=new_users, x='device',)
plt.title('Устройства, с которых проходила регистрация', fontsize=15)
plt.xlabel('Устройства')
plt.ylabel('Число пользователей')
plt.show()
# девайсы в % от общего
device = new_users['device'].value_counts().reset_index()
device.columns = ['device', 'count']
device['% of total']= round(device['count'] / device['count'].sum() * 100, 1)
device
| device | count | % of total | |
|---|---|---|---|
| 0 | Android | 18828 | 44.5 |
| 1 | PC | 10760 | 25.4 |
| 2 | iPhone | 8591 | 20.3 |
| 3 | Mac | 4161 | 9.8 |
~ 45 % пользователей из EU зарегистрировалась через ОС Android, треть - пользователи Apple (iPhone - ~20 %, Mac - ~10 %). Четверть пользователей предпочитают PC.
Календарь маркетинговых событий
Посмотрим еще раз на календарь маркетинговых событий. Проверим, не совпадает ли время проведения теста с маркетинговыми активностями.
marketing_events
| name | regions | start_dt | finish_dt | |
|---|---|---|---|---|
| 0 | Christmas&New Year Promo | EU, N.America | 2020-12-25 | 2021-01-03 |
| 1 | St. Valentine's Day Giveaway | EU, CIS, APAC, N.America | 2020-02-14 | 2020-02-16 |
| 2 | St. Patric's Day Promo | EU, N.America | 2020-03-17 | 2020-03-19 |
| 3 | Easter Promo | EU, CIS, APAC, N.America | 2020-04-12 | 2020-04-19 |
| 4 | 4th of July Promo | N.America | 2020-07-04 | 2020-07-11 |
| 5 | Black Friday Ads Campaign | EU, CIS, APAC, N.America | 2020-11-26 | 2020-12-01 |
| 6 | Chinese New Year Promo | APAC | 2020-01-25 | 2020-02-07 |
| 7 | Labor day (May 1st) Ads Campaign | EU, CIS, APAC | 2020-05-01 | 2020-05-03 |
| 8 | International Women's Day Promo | EU, CIS, APAC | 2020-03-08 | 2020-03-10 |
| 9 | Victory Day CIS (May 9th) Event | CIS | 2020-05-09 | 2020-05-11 |
| 10 | CIS New Year Gift Lottery | CIS | 2020-12-30 | 2021-01-07 |
| 11 | Dragon Boat Festival Giveaway | APAC | 2020-06-25 | 2020-07-01 |
| 12 | Single's Day Gift Promo | APAC | 2020-11-11 | 2020-11-12 |
| 13 | Chinese Moon Festival | APAC | 2020-10-01 | 2020-10-07 |
Для наглядности построим по даным таблицы диаграмму Ганта:
# диаграмма Ганта календарь событий
fig = px.timeline(marketing_events, x_start='start_dt', x_end='finish_dt', y='name', title = 'Каледарь маркетинговых событий')
fig.update_yaxes(autorange='reversed')
fig.show()
Проведение теста приходится на 07.12.2020 - 04.01.2021 гг. Исходя из диаграммы, на это время накладываются два события, связанных с Рождеством и Новым годом - Christmas&New Year Promo и CIS New Year Gift Lottery. Уточним, действительно ли это так:
# маркетинговые события продолжающиеся после начала теста
marketing_events.query('start_dt >= "2020-12-07"')
| name | regions | start_dt | finish_dt | |
|---|---|---|---|---|
| 0 | Christmas&New Year Promo | EU, N.America | 2020-12-25 | 2021-01-03 |
| 10 | CIS New Year Gift Lottery | CIS | 2020-12-30 | 2021-01-07 |
Так и есть. При этом пользователей из EU касается только одно событие 'Christmas&New Year Promo'.
Довольно странно, что время проведения теста приходится на предновогоднии дни, сезонный всплеск активности клиентов и без проведения маркетинговых мероприятий может исказить результаты теста.
Участники теста
participants
| user_id | group | ab_test | |
|---|---|---|---|
| 0 | D1ABA3E2887B6A73 | A | recommender_system_test |
| 1 | A7A3664BD6242119 | A | recommender_system_test |
| 2 | DABC14FDDFADD29E | A | recommender_system_test |
| 3 | 04988C5DF189632E | A | recommender_system_test |
| 4 | 482F14783456D21B | B | recommender_system_test |
| ... | ... | ... | ... |
| 18263 | 1D302F8688B91781 | B | interface_eu_test |
| 18264 | 3DE51B726983B657 | A | interface_eu_test |
| 18265 | F501F79D332BE86C | A | interface_eu_test |
| 18266 | 63FBE257B05F2245 | A | interface_eu_test |
| 18267 | 79F9ABFB029CF724 | B | interface_eu_test |
18268 rows × 3 columns
# число уникальных пользователей
participants['user_id'].nunique()
16666
Число уникальных пользователей теста меньше, чем количество строк в таблице. Это говорит о том, что некоторые пользователи попали сразу в несколько тестов или групп. Выясним, в чем дело:
# уникальные значения в столбце Группа пользователя и их повторяемость
participants['group'].value_counts()
A 9655 B 8613 Name: group, dtype: int64
# разница в % между группами А и В
round(((1 - participants[participants['group'] == "B"]['group'].count() / participants[participants['group'] == "A"]['group'].count()) * 100), 1)
10.8
Группы А и В по числу участников различаются на ~ 11 %.
# распределение пользователей между тестами
participants['ab_test'].value_counts()
interface_eu_test 11567 recommender_system_test 6701 Name: ab_test, dtype: int64
Помимо интересующего нас теста 'recommender_system_test' в данных обнаружен второй тест 'interface_eu_test'. Удостоверимся, что с конкурирующим тестом нет пересечений по пользователям:
# группировка и фильтрация пользователей по присутствию в двух тестах одновременно
len(participants.groupby('user_id').agg({'ab_test': 'nunique'}).reset_index().query('ab_test == 2'))
1602
1602 пользователя участвуют сразу в двух тестах, отберем пользователей только по тесту 'recommender_system_test':
# фильтрация тестов
participants = participants.query('ab_test == "recommender_system_test"')
participants['ab_test'].value_counts()
recommender_system_test 6701 Name: ab_test, dtype: int64
Проверим, среди оставшихся пользователей имеются ли те, кто участвует в двух группах теста:
# пользователи попавшие и в А и В группу
len(participants.groupby('user_id').agg({'group': 'nunique'}).reset_index().query('group == 2'))
0
Пользователей, участвующих сразу в двух группах теста, нет. Посмотрим, как изменилось распределение пользователей между группами после исключения из данных теста 'interface_eu_test':
# уникальные значения в столбце Группа пользователя и их повторяемость
participants['group'].value_counts()
A 3824 B 2877 Name: group, dtype: int64
# разница в % между группами А и В
round(((1 - participants[participants['group'] == "B"]['group'].count() / participants[participants['group'] == "A"]['group'].count()) * 100), 1)
24.8
Пользователи в тестовых группах распределены неравномерно, разница между ними составляет ~ 25 %.
Посчитаем количество новых пользователей - участников теста из EU и их процент от общего числа новых пользователей:
# участники из EU после чистки данных
print('Количество новых уникальных пользователей из EU:', len(list(set(participants['user_id']) & set(new_users['user_id']))))
# % участников из EU после чистки от общего числа участников из EU до чистки
print('% новых уникальных пользователей из EU от общего числа новых пользователей из всех регионов:', round((len(list(set(participants['user_id']) & set(new_users['user_id']))) / 46270)*100, 2))
Количество новых уникальных пользователей из EU: 6351 % новых уникальных пользователей из EU от общего числа новых пользователей из всех регионов: 13.73
Условие по ожидаемому количеству участников теста соблюдено.
А вот по аудитории нет: % новых пользователей из региона EU получился меньше заявленного в ТЗ и составил ~14 %.
Действия новых пользователей в период с 7 декабря 2020 по 4 января 2021 года
events
| user_id | event_dt | event_name | details | |
|---|---|---|---|---|
| 0 | E1BDDCE0DAFA2679 | 2020-12-07 20:22:03 | purchase | 99.99 |
| 1 | 7B6452F081F49504 | 2020-12-07 09:22:53 | purchase | 9.99 |
| 2 | 9CD9F34546DF254C | 2020-12-07 12:59:29 | purchase | 4.99 |
| 3 | 96F27A054B191457 | 2020-12-07 04:02:40 | purchase | 4.99 |
| 4 | 1FD7660FDF94CA1F | 2020-12-07 10:15:09 | purchase | 4.99 |
| ... | ... | ... | ... | ... |
| 440312 | 245E85F65C358E08 | 2020-12-30 19:35:55 | login | NaN |
| 440313 | 9385A108F5A0A7A7 | 2020-12-30 10:54:15 | login | NaN |
| 440314 | DB650B7559AC6EAC | 2020-12-30 10:59:09 | login | NaN |
| 440315 | F80C9BDDEA02E53C | 2020-12-30 09:53:39 | login | NaN |
| 440316 | 7AEC61159B672CC5 | 2020-12-30 11:36:13 | login | NaN |
440317 rows × 4 columns
# общее число действий в логе
print('Общее число действий:', events['user_id'].count())
# количество уникальных значений в столбце user_id
print('Количество уникальных пользователей, совершивших действие:', events['user_id'].nunique())
# число действий на уникального пользователя
print('Количество действий на уникального пользователя:', round(events['user_id'].count() / events['user_id'].nunique(), 2))
Общее число действий: 440317 Количество уникальных пользователей, совершивших действие: 58703 Количество действий на уникального пользователя: 7.5
На каждого уникального пользователя приходится ~ 8 действий.
Посмотрим, как число событий распределено во времени:
# минимальное время и дата
display(events['event_dt'].min())
# максимальное время и дата
events['event_dt'].max()
Timestamp('2020-12-07 00:00:33')
Timestamp('2020-12-30 23:36:33')
# добавление столбца даты регистрации для получения даты без времени 00:00:00
events['purchase_date'] = events['event_dt'].dt.date
# столбчатая диаграмма Распределение событий во времени
sns.set_theme(style="whitegrid")
events.groupby('purchase_date').agg({'event_name': 'count'}).plot(kind = 'bar', figsize = (15, 5))
plt.title('Распределение событий во времени', fontsize=15)
plt.xlabel('Дата')
plt.ylabel('Количество событий')
plt.xticks(rotation = 30)
plt.show()
Удивительно, что последнее действие пользователей датируется 30 декабря 2020 года, что противоречит документации, в которой правая временная граница обозначена, как 04 января 2021 года.
Для дальнейшего проведения исследовательского анализа объединим таблицы 'events', 'participants' и 'new_users' в одну. Предварительно посмотрим на пересечение трех датасетов, это даст нам информацию о числе уникальных пользователей, которое должно совпасть с числом уникальных пользователей после слияния таблиц.
# пересечение трех множеств (количество пользователей, общее для 3-х датасетов)
len(list(set(events['user_id']) & set(participants['user_id']) & set(new_users['user_id'])))
3481
Получили 3481 уникального пользователя.
# объединение 'events' и 'participants'
events = events.merge(participants, on = 'user_id', how = 'inner')
# объединение 'events' и 'new_users'
events = events.merge(new_users, on = 'user_id', how = 'inner')
events
| user_id | event_dt | event_name | details | purchase_date | group | ab_test | first_date | region | device | |
|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 831887FE7F2D6CBA | 2020-12-07 06:50:29 | purchase | 4.99 | 2020-12-07 | A | recommender_system_test | 2020-12-07 | EU | Android |
| 1 | 831887FE7F2D6CBA | 2020-12-09 02:19:17 | purchase | 99.99 | 2020-12-09 | A | recommender_system_test | 2020-12-07 | EU | Android |
| 2 | 831887FE7F2D6CBA | 2020-12-07 06:50:30 | product_cart | NaN | 2020-12-07 | A | recommender_system_test | 2020-12-07 | EU | Android |
| 3 | 831887FE7F2D6CBA | 2020-12-08 10:52:27 | product_cart | NaN | 2020-12-08 | A | recommender_system_test | 2020-12-07 | EU | Android |
| 4 | 831887FE7F2D6CBA | 2020-12-09 02:19:17 | product_cart | NaN | 2020-12-09 | A | recommender_system_test | 2020-12-07 | EU | Android |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 23415 | 1484BBF124DB1B18 | 2020-12-27 10:53:34 | login | NaN | 2020-12-27 | A | recommender_system_test | 2020-12-21 | EU | PC |
| 23416 | BEF16764A13AEC34 | 2020-12-21 03:49:49 | login | NaN | 2020-12-21 | B | recommender_system_test | 2020-12-21 | EU | PC |
| 23417 | BEF16764A13AEC34 | 2020-12-22 18:52:25 | login | NaN | 2020-12-22 | B | recommender_system_test | 2020-12-21 | EU | PC |
| 23418 | BEF16764A13AEC34 | 2020-12-24 22:11:00 | login | NaN | 2020-12-24 | B | recommender_system_test | 2020-12-21 | EU | PC |
| 23419 | 23DDD27AC3FEFA63 | 2020-12-21 02:51:45 | login | NaN | 2020-12-21 | A | recommender_system_test | 2020-12-21 | EU | PC |
23420 rows × 10 columns
# количество полных дубликатов
events.duplicated().sum()
0
# уникальные пользователи, состоящие в нескольких группах одновременно
events.groupby('user_id')['group'].agg('nunique').reset_index().query('group > 1')
| user_id | group |
|---|
Объединенная таблица содержит 23 420 строк и 10 столбцов. Полные дубликаты не обнаружены. Пользователи состоящие сразу в нескольких группах отсутствуют.
# общее число действий в итоговой таблице
print('Общее число действий:', events['user_id'].count())
# количество уникальных значений в столбце user_id
print('Количество уникальных пользователей, совершивших действие:', events['user_id'].nunique())
# число действий на уникального пользователя
print('Количество действий на уникального пользователя:', round(events['user_id'].count() / events['user_id'].nunique(), 2))
Общее число действий: 23420 Количество уникальных пользователей, совершивших действие: 3481 Количество действий на уникального пользователя: 6.73
Как и было подсчитано выше, в итоговой таблице обнаружено 3481 уникальных пользователя, совершивших действие. До объединения таблиц число уникальных пользователей было 6351. Получается, что 2870 пользователей действий не совершали. Таким образом, условие по ожидаемому количеству участников теста все таки не соблюдается.
При этом, кого считать участником теста: того, кто зарегистрировался или того, кто прошел авторизацию? Остается вопросом.
На каждого уникального пользователя приходится ~ 7 действий. Посмотрим, одинаково ли распределены события в выборках:
# группировка уникальных пользователей по группам
users_by_group = events.groupby('group')['user_id'].nunique().reset_index()
users_by_group.columns = ['group', 'users_nunique']
# группировка всех пользователей по группам
users_by_group_count = events.groupby('group')['user_id'].count().reset_index()
users_by_group_count.columns = ['group', 'users_count']
# объединение двух таблиц
event_count_group = users_by_group.merge(users_by_group_count, on = 'group', how = 'inner')
event_count_group['event_count'] = round(event_count_group['users_count'] / event_count_group['users_nunique'], 2)
display(event_count_group)
# барплот 'Распределение количества действий на пользователя по группам'
sns.set_theme(style="whitegrid")
plt.figure(figsize=(15,5))
sns.barplot(data=event_count_group, x='group', y = 'event_count',)
plt.title('Распределение событий на уникального пользователя в группах', fontsize=15)
plt.xlabel('Группа')
plt.ylabel('Количество действий')
plt.xticks(rotation = 0)
plt.show()
| group | users_nunique | users_count | event_count | |
|---|---|---|---|---|
| 0 | A | 2604 | 18309 | 7.03 |
| 1 | B | 877 | 5111 | 5.83 |
В контрольной группе А на одного уникального пользователя приходится ~ 7 действий, в новой группе В меньше, ~ 6 действий.
Исследуем, как число событий в выборках распределено по дням:
# столбчатая диаграмма Распределение событий во времени по группам
sns.set_theme(style="whitegrid")
ax = events.query('group == "A"').groupby('purchase_date').agg({'event_name': 'count'}).plot(kind = 'bar', figsize = (15, 5))
events.query('group == "B"').groupby('purchase_date').agg({'event_name': 'count'}).plot(kind = 'bar', ax=ax, color='red', alpha=0.6)
plt.title('Распределение событий во времени по группам', fontsize=15)
plt.xlabel('Дата')
plt.ylabel('Количество событий')
plt.xticks(rotation = 30)
plt.legend(['A', 'B'])
plt.show()
Столбчатая диаграмма наглядно отражает сильное различие в числе событий между двумя группами: в новой группе В существенно меньше событий.
В контрольной группе А с начала теста и по 13.12.2020 года активность не наблюдается, после чего происходит резкий скачок, который можно объяснить подготовкой пользователей к праздникам. Пик событий приходится на 21.12.2020, затем виден плавный спад.
Если про контрольную группу еще можно сказать, что распределение данных похоже на нормальное, то в новой группе В, события распределяются очень странным образом. Количество событий то нарастает, то падает. Днями, когда происходит наибольшее число событий, можно считать: 21 декабря, 16 декабря , 07 декабря и 09 декабря. После 21 декабря наблюдается резкий спад.
Возможно, что во время проведения теста произошли технические сбои или вероятно, нам не догрузили данные.
Посмотрим, какие события встречаются в логах:
Встречаются следующие типы событий:
1) login - авторизация;
2) product_page - просмотр карточек товаров;
3) purchase - покупка;
4) product_cart - просмотр корзины.
Посчитаем, сколько пользователей совершали каждое из этих событий:
# количество пользователей совершивших каждое из этих событий
funnel = events.groupby('event_name')['user_id'].nunique().reset_index().rename(columns = {'user_id': 'user_count'}).sort_values(by = 'user_count', ascending = False)
# процент пользователей, хоть раз совершивших событие
funnel['percent'] = round((funnel['user_count'] / events['user_id'].nunique() * 100), 1)
funnel
| event_name | user_count | percent | |
|---|---|---|---|
| 0 | login | 3481 | 100.0 |
| 2 | product_page | 2178 | 62.6 |
| 3 | purchase | 1082 | 31.1 |
| 1 | product_cart | 1026 | 29.5 |
Построим воронку событий:
# воронка событий plotly
fig = go.Figure(
go.Funnel(
y = funnel['event_name'],
x = funnel['user_count'],
textinfo = "value+percent previous+percent initial",
)
)
fig.update_layout(
title={
'text': 'Воронка событий',
'y':0.9,
'x':0.5,
'xanchor': 'center',
'yanchor': 'top'
}
)
fig.show()
По воронке событий наглядно видно, какая доля пользователей проходит на следующий шаг воронки (от числа пользователей на предыдущем):
Количество уникальных пользователей в датасете равно числу 'Авторизовавшихся' (3481);
'Просмотрели карточки товаров' 62.6 % пользователей от 'Авторизовавшихся';
'Совершили покупку' 49.7% от 'Просмотревших карточку' и 31.1 % от 'Авторизовавшихся';
'Просмотрели корзину' 94.8 % от 'Совершивших покупку' и 29.5 % от 'Авторизовавшихся'.
Вызывает интерес тот факт, что 56 покупок совершены в обход корзины. Это может быть связано, как с технологической ошибкой, так и с возможностью купить товар в один клик. По этой причине шаг 'просмотр корзины' расположен в самом низу.
Таким образом, больше всего пользователей теряется на шаге 'Авторизация' - около 37 %.
От первого события до оплаты доходит примерно 31 % пользователей.
Построим воронку событий по группам и посмотрим, как меняется конверсия в выборках на разных этапах:
# воронка событий в разбивке по группам
group_funnel = events.pivot_table(index = 'event_name', columns = 'group', values = 'user_id', aggfunc = 'nunique').reset_index()
group_funnel.columns = ['event_name', 'A', 'B',]
group_funnel = group_funnel.sort_values(by = 'A', ascending=False)
group_funnel
| event_name | A | B | |
|---|---|---|---|
| 0 | login | 2604 | 877 |
| 2 | product_page | 1685 | 493 |
| 3 | purchase | 833 | 249 |
| 1 | product_cart | 782 | 244 |
# воронка событий plotly
fig = go.Figure()
fig.add_trace(go.Funnel(
name = 'A',
y = group_funnel['event_name'],
x = group_funnel['A'],
textinfo = "value+percent previous+percent initial"))
fig.add_trace(go.Funnel(
name = 'B',
y = group_funnel['event_name'],
x = group_funnel['B'],
textinfo = "value+percent previous+percent initial",
textposition = "inside"))
fig.update_layout(
title={
'text': 'Воронка событий по группам',
'y':0.9,
'x':0.5,
'xanchor': 'center',
'yanchor': 'top',
}
)
fig.show()
Самое популярное событие - просмотр карточек товаров (product_page). В группе A его совершили 1685 пользователей (65 %), в группе B: 493 пользователя (56 %).
# улучшение метрик за все время проведения теста
group_funnel['%_of_initial_A'] = round(group_funnel['A'] / group_funnel.loc[0,'A'], 3)
group_funnel['%_of_initial_B'] = round(group_funnel['B'] / group_funnel.loc[0,'B'], 3)
group_funnel['metric_change_%'] = round((group_funnel['%_of_initial_B'] / group_funnel['%_of_initial_A'] - 1) * 100, 2)
group_funnel
| event_name | A | B | %_of_initial_A | %_of_initial_B | metric_change_% | |
|---|---|---|---|---|---|---|
| 0 | login | 2604 | 877 | 1.000 | 1.000 | 0.00 |
| 2 | product_page | 1685 | 493 | 0.647 | 0.562 | -13.14 |
| 3 | purchase | 833 | 249 | 0.320 | 0.284 | -11.25 |
| 1 | product_cart | 782 | 244 | 0.300 | 0.278 | -7.33 |
За все время проведения теста (07.12.2020 - 30.12.2020) получен следующий эффект:
конверсия в просмотр карточек товаров — событие product_page — ухудшение метрики на 13 %,
просмотры корзины — product_cart — ухудшение метрики на 7 %,
покупки — purchase — ухудшение метрики на 11 %.
Если такой отрицательный результат получен за 24 дня, ни о каком улучшении каждой метрики не менее, чем на 10 % за 14 дней, не может быть и речи.
Прежде чем приступать к А/В-тестированию следовало учесть, корректность деления трафика между группами. Например, недопустимо, чтобы пользователи группы A — были посетителями мобильной версии сайта, а пользователи группы B — десктопной. Проверим, как обстоят дела с распределением пользователей по устройствам в группах:
# группировка уникальных пользователей по устройствам и группам
group_by_device = events.groupby(['device', 'group'])['user_id'].nunique().reset_index()
# объединение двух таблиц по столбцу группа и переименование столбцов
group_by_device = group_by_device.merge(users_by_group, on = 'group', how = 'inner')
group_by_device.columns = ['device', 'group', 'users_in_group', 'total_in_group']
# добавление столбца доля пользователей, использующих устройство от общего в группе
group_by_device['rate'] = round(group_by_device['users_in_group'] / group_by_device['total_in_group'], 3)
display(group_by_device)
# барплот 'Распределение уникальных пользователей по устройствам с учетом группы'
sns.set_theme(style="whitegrid")
plt.figure(figsize=(15,5))
sns.barplot(data=group_by_device, x="device", y = 'rate', hue="group",)
plt.title('Распределение уникальных пользователей по устройствам в группах', fontsize=15)
plt.xlabel('Устройства')
plt.ylabel('Доля пользователей')
plt.xticks(rotation = 0)
plt.show()
| device | group | users_in_group | total_in_group | rate | |
|---|---|---|---|---|---|
| 0 | Android | A | 1139 | 2604 | 0.437 |
| 1 | Mac | A | 255 | 2604 | 0.098 |
| 2 | PC | A | 689 | 2604 | 0.265 |
| 3 | iPhone | A | 521 | 2604 | 0.200 |
| 4 | Android | B | 405 | 877 | 0.462 |
| 5 | Mac | B | 74 | 877 | 0.084 |
| 6 | PC | B | 212 | 877 | 0.242 |
| 7 | iPhone | B | 186 | 877 | 0.212 |
Между долями пользователей устройств имеются несущественные различия по группам.
1) Данные не соответствуют требованиям технического задания:
помимо интересующего нас теста 'recommender_system_test' в данных обнаружен второй тест 'interface_eu_test', с которым имеются пересечения. Пользователей, участвующих сразу в двух группах теста, нет;
дата остановки набора новых пользователей отличается - 2020-12-23. С датой начала набора все в порядке;
время проведения теста совпадает с событием 'Christmas&New Year Promo', это может повлиять на достоверность результата теста;
пользователи в контрольной группе А и новой группе В (новая платежная воронка) распределены неравномерно, и после объединения данных в одну таблицу разница между ними составляет ~ 34 %;
условие по ожидаемому количеству участников теста не соблюдено (3481). % новых пользователей из региона EU также получился меньше заявленного и составил ~14 %;
последнее действие пользователей датируется 30 декабря 2020 года, что противоречит документации, в которой датой остановки теста указано 04 января 2021 года;
время проведения теста выбрано неудачно, т.к. приходится на предновогоднии дни, сезонный всплеск активности клиентов и без проведения маркетинговых мероприятий может исказить результаты теста.
2) Из результатов исследовательского анализа следует:
в контрольной группе А на одного уникального пользователя приходится ~ 7 действий, в новой группе В меньше, ~ 6 действий;
56 покупок совершены в обход корзины. Это может быть связано, как с технологической ошибкой, так и с возможностью купить товар в один клик. От первого события до оплаты доходит примерно 31 % пользователей;
ожидаемый эффект — улучшение каждой метрики не менее, чем на 10 %, не достигнут за все время проведения теста (07.12.2020 - 30.12.2020). Нет смысла проверять результат за 14 дней. Получены следующие результаты:
конверсия в просмотр карточек товаров — ухудшение метрики на 13 %,
просмотры корзины — ухудшение метрики на 7 %,
покупки — ухудшение метрики на 11 %.
при регистрации клиенты использовали: Android, PC, iPhone и Mac. Между долями пользователей устройств имеются несущественные различия по группам.
Полученные результаты позволяют сделать вывод о некорректности проведенного теста.
Проверим статистическую разницу долей z-критерием.
Сформулируем нулевую и альтернативную гипотезы:
Н0 (Нулевая гипотеза): между выборками A и В нет отличий в доле пользователей, совершивших событие Х;
Н1 (Альтернативная гипотеза): между выборками A и В есть отличия в доле пользователей, совершивших событие Х.
Примем критический уровень статистической знаимости (alpha) равным 0.05.
# воронка событий по группам
ab_funnel = events.pivot_table(index = 'event_name', columns = 'group', values = 'user_id', aggfunc = 'nunique').sort_values(by = 'B', ascending = False)
ab_funnel = ab_funnel.query('event_name != "login"')
ab_funnel
| group | A | B |
|---|---|---|
| event_name | ||
| product_page | 1685 | 493 |
| purchase | 833 | 249 |
| product_cart | 782 | 244 |
# количество пользователей в каждой группе
trials = users_by_group
trials = trials.set_index(trials.columns[0])
trials
| users_nunique | |
|---|---|
| group | |
| A | 2604 |
| B | 877 |
# исследование отличий для групп A и B по событиям
def z_test (exp1, exp2, event, alpha):
successes1 = ab_funnel.loc[event, exp1]
successes2 = ab_funnel.loc[event, exp2]
trials1 = trials.loc[exp1, 'users_nunique']
trials2 = trials.loc[exp2, 'users_nunique']
# пропорция успехов в обеих группах:
p1 = successes1/trials1
p2 = successes2/trials2
# пропорция успехов в комбинированном датасете:
p_combined = (successes1 + successes2) / (trials1 + trials2)
# разница пропорций в датасетах
difference = p1 - p2
# считаем статистику в ст.отклонениях стандартного нормального распределения
z_value = difference / mth.sqrt(p_combined * (1 - p_combined) * (1/trials1 + 1/trials2))
# задаем стандартное нормальное распределение (среднее 0, ст.отклонение 1)
distr = st.norm(0, 1)
p_value = (1 - distr.cdf(abs(z_value))) * 2
print('Для групп {} и {} по событию {} p-значение: {p_value:.2f}'.format(exp1, exp2, event, p_value=p_value))
if (p_value < alpha):
print("Отвергаем нулевую гипотезу: между долями есть значимая разница")
else:
print("Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными")
# вызов функции z-test для A и B групп
for event in ab_funnel.index:
z_test("A", "B", event, 0.05)
print()
Для групп A и B по событию product_page p-значение: 0.00 Отвергаем нулевую гипотезу: между долями есть значимая разница Для групп A и B по событию purchase p-значение: 0.05 Отвергаем нулевую гипотезу: между долями есть значимая разница Для групп A и B по событию product_cart p-значение: 0.21 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными
По событиям product_page и purchase отвергаем нулевую гипотезу, между долями есть статистическая значимость.
По событию product_cart не получилось отвергнуть нулевую гипотезу, статистической разницы между долями нет.
В исследовании проведено 3 сравнения, уровень значимости был принят равным 0.05. Чтобы снизить вероятность ложнопозитивного результата при множественном тестировании гипотез, применяют разные методы корректировки уровня значимости для уменьшения FWER. Из-за простоты решения применим поправку Бонфферони:
print('Поправка Бонфферони для 3 сравнений с уровнем значимости 0.05:', round(0.05/3, 6))
Поправка Бонфферони для 3 сравнений с уровнем значимости 0.05: 0.016667
# вызов функции z-test для A и B групп
for event in ab_funnel.index:
z_test("A", "B", event, 0.016667)
print()
Для групп A и B по событию product_page p-значение: 0.00 Отвергаем нулевую гипотезу: между долями есть значимая разница Для групп A и B по событию purchase p-значение: 0.05 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными Для групп A и B по событию product_cart p-значение: 0.21 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными
От изменения уровня значимости p-value не зависит, поэтому p-значения остались такими же, а вот результаты сравнения получены другие:
после применения поправки Бонфферони по событию purchase не получилось отвергнуть нулевую гипотезу, между долями нет статистической разницы.
Проверка статистической разницы долей z-критерием показала, что:
между долями контрольной группы А и новой группы В по событию просмотр карточек товаров есть статистическая значимость;
по событиям покупка и просмотр корзины статистической разницы между долями групп А и В нет. Разница в конверсии по покупкам (-11.25 %) и просмотрам корзины (-7.33 %), расчитанная на этапе EDA, могла быть получена случайно.
На основании полученных результатов можно заключить, что тест проведен некорректно:
пользователи в контрольной группе А и новой группе В (новая платежная воронка) распределены неравномерно, разница между ними составляет ~ 34 %;
в данных присутсвует второй тест 'interface_eu_test', с которым имеются пересечения;
время проведения теста совпадает с событием 'Christmas&New Year Promo';
время проведения теста (2020-12-07 - 2020-12-30) отличается от заявленного и накладывается на Новогоднее и Рождественское время в EU;
условие по ожидаемому количеству участников теста не соблюдено (3481). % новых пользователей из региона EU также получился меньше заявленного и составил ~14 %.
За фактическое время проведения теста улучшение каждой метрики не менее, чем на 10 %, не достигнуто, наоборот, получен отрицательный эффект.
Проверка статистической разницы долей z-критерием показала, что:
между долями контрольной группы А и новой группы В по событию просмотр карточек товаров есть статистическая значимость;
по событиям покупка и просмотр корзины статистической разницы между долями групп А и В нет. Разница в конверсии по покупкам (-11.25 %) и просмотрам корзины (-7.33 %), могла быть получена случайно.
В качестве рекомендаций можно предложить:
следить за корректным делением трафика теста;
сопоставлять даты проведения теста с календарем маркетинговых событий;
брать в расчет всплески активности конкретно своей аудитории (сезонность, праздники, выходные дни и т.д.).